#region Copyright
//       Copyright (c) 2009 - Antoine Jaussoin - http://www.jaussoin.com/blog/
// 
//        This program is free software: you can redistribute it and/or modify
//        it under the terms of the GNU General Public License as published by
//        the Free Software Foundation, either version 3 of the License, or
//        (at your option) any later version.
// 
//        This program is distributed in the hope that it will be useful,
//        but WITHOUT ANY WARRANTY; without even the implied warranty of
//        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//        GNU General Public License for more details.
// 
//        You should have received a copy of the GNU General Public License
//        along with this program.  If not, see <http://www.gnu.org/licenses/>.
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using FT.Architecture.Logging;
using FT.Architecture.Repositories;
using FT.Architecture.NHibernate.SessionManagement;
using FT.Architecture.Utils.ExtensionMethods;
using NHibernate.Proxy;

namespace FT.Architecture.NHibernate.Repositories
{
    /// <summary>
    /// <see cref="NHibernate"/> implementation of a Non Generic Repository
    /// </summary>
    public class NHibernateNonGenericRepository : INonGenericRepository
    {
        private readonly ISessionProviderService sessionProviderService;
        private readonly ILogService logService;

        public NHibernateNonGenericRepository(ISessionProviderService sessionProviderService, ILogService logService)
        {
            this.sessionProviderService = sessionProviderService;
            this.logService = logService;
        }

        /// <summary>
        /// Get an object by its id and its type
        /// </summary>
        /// <param name="id"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public object GetById(object id, Type type)
        {
            return SessionProvider.GetSession().CreateQuery("from " + type.Name + " e where e.id = :id")
                .SetParameter("id", id)
                .UniqueResult();
        }

        /// <summary>
        /// Get an object by the value of one of its member
        /// </summary>
        /// <param name="memberValue"></param>
        /// <param name="memberName"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public object GetByMember(object memberValue, string memberName, Type type)
        {
            return SessionProvider.GetSession().CreateQuery(string.Format("from {0} o where o.{1} = :value", type.Name, memberName))
                .SetParameter("value", memberValue)
                .UniqueResult();
        }

        /// <summary>
        /// Get all entities of the specified type
        /// </summary>
        /// <returns></returns>
        public IList GetAll(Type type)
        {
            return SessionProvider.GetSession().CreateQuery("from " + type.Name)
                .List();
        }

        /// <summary>
        /// Returns the number of entities of a given type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public long GetCount(Type type)
        {
            logService.Repository.Info("Get Total count " + type.Name);

            // We have to use Linq here because the NH query sometimes return more than one result (if the type span on multiple tables)
            var results = (
                from long count in SessionProvider.GetSession().CreateQuery("select count(*) from " + type.Name + " e").List<long>()
                select count).Sum();

            return results;
        }

        /// <summary>
        /// Get a random object of the given type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public object GetRandom(Type type)
        {
            logService.Repository.Info("Get Random Entity " + type.Name);

            var random = new Random((int)DateTime.Now.Ticks);
            long count = GetCount(type);
            return SessionProvider.GetSession().CreateQuery("from " + type.Name)
                .SetFirstResult(random.Next(0, (int)count))
                .SetMaxResults(1)
                .UniqueResult();
        }

        /// <summary>
        /// Save an entity: Make it persistent, as opposed to transient
        /// </summary>
        /// <param name="entity"></param>
        public void Save(object entity)
        {
            logService.Repository.Info(string.Format("Save entity {0}, {1}", entity.GetType().Name, entity));

            SessionProvider.GetSession().SaveOrUpdate(entity);
        }

        /// <summary>
        /// Delete an entity, but doesn't null the ID on it.
        /// </summary>
        /// <param name="entity"></param>
        public void Delete(object entity)
        {
            logService.Repository.Info(string.Format("Delete entity {0}, {1}, {2}", entity.GetType().Name, entity));

            // Delete the object from the database
            SessionProvider.GetSession().Delete(entity);
        }

        /// <summary>
        /// Un-proxy an object
        /// </summary>
        /// <param name="proxiedEntity"></param>
        /// <returns></returns>
        public object UnProxy(object proxiedEntity)
        {
            //If the object is not a proxy, just cast it and returns it
            if (!(proxiedEntity is INHibernateProxy))
                return proxiedEntity;

            //Otherwise, use the NHibernate methods to get the implementation, and cast it
            var p = (INHibernateProxy)proxiedEntity;
            return p.HibernateLazyInitializer.GetImplementation();
        }

        /// <summary>
        /// Get the concrete class of an object, from a proxied version
        /// </summary>
        /// <param name="proxiedEntity"></param>
        /// <returns></returns>
        public Type GetConcreteType(object proxiedEntity)
        {
            return NHibernateProxyHelper.GuessClass(proxiedEntity);
        }

        public ISessionProviderService SessionProvider
        {
            get { return sessionProviderService; }
        }

        /// <summary>
        /// Search a list of entities. This is mainly used by <c>NSelector</c>
        /// </summary>
        /// <param name="searchText"></param>
        /// <param name="propertiesToSearchOn"></param>
        /// <param name="displayProperties"></param>
        /// <param name="orderByProperty"></param>
        /// <param name="orderByDirection"></param>
        /// <param name="type"></param>
        /// <param name="firstResult"></param>
        /// <param name="maxResult"></param>
        /// <returns></returns>
        public IList SearchInObject(string searchText, string propertiesToSearchOn, string displayProperties,
                                    string orderByProperty, string orderByDirection, string type,
                                    int firstResult, int maxResult)
        {
            var searchProperties = new List<string>(propertiesToSearchOn.Split(';'));

            string joinClause = GetJoins(propertiesToSearchOn, displayProperties);

            if (!string.IsNullOrEmpty(searchText.Replace("*", "")))
            {
                string query = string.Format("select o from {0} o {1}where (", type, joinClause);

                foreach (string searchProperty in searchProperties)
                {
                    query += GetPropertyClause(searchProperty);
                }

                //Remove the last or
                query = query.Remove(query.Length - 3);
                query += ") ";


                query += string.Format("order by o.{0} {1}", orderByProperty, orderByDirection);

                return SessionProvider.GetSession().CreateQuery(query)
                    .SetParameter("searchText", searchText.Replace('*', '%') + "%")
                    .SetFirstResult(firstResult)
                    .SetMaxResults(maxResult)
                    .List();
            }
            else
            {
                string query = string.Format("select o from {0} o {1} ", type, joinClause);


                query += string.Format("order by o.{0} {1}", orderByProperty, orderByDirection);

                return SessionProvider.GetSession().CreateQuery(query)
                    .SetFirstResult(firstResult)
                    .SetMaxResults(maxResult)
                    .List();
            }
        }

        /// <summary>
        /// Count the number of objects matching the <paramref name="searchText"/> for the given type and the given property
        /// </summary>
        /// <param name="searchText"></param>
        /// <param name="propertiesToSearchOn"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public int CountInObject(string searchText, string propertiesToSearchOn, string type)
        {
            var searchProperties = new List<string>(propertiesToSearchOn.Split(';'));

            string joinClause = GetJoins(propertiesToSearchOn, "");

            string query = string.Format("select count(o.id) from {0} o {1}where (", type, joinClause);

            foreach (string searchProperty in searchProperties)
            {
                query += GetPropertyClause(searchProperty);
            }

            //Remove the last or
            query = query.Remove(query.Length - 3);
            query += ") ";

            int count = (int)SessionProvider.GetSession().CreateQuery(query)
                                  .SetParameter("searchText", searchText.Replace('*', '%') + "%")
                                  .UniqueResult<long>();

            return count;
        }

        /// <summary>
        /// Transform a property into a full clause : "Universe.Name" --> "o.Universe.Name like :searchText or "
        /// </summary>
        /// <param name="property"></param>
        /// <returns></returns>
        protected static string GetPropertyClause(string property)
        {
            return string.Format("o.{0} like :searchText or ", property);
        }

        /// <summary>
        /// Extract part of a complex property. For example, if property is "Universe.Parent.Parent.Name", calling this method with a stop at 1 will return "Universe.Parent", while a stop at 2 will return "Universe.Parent.Parent".
        /// </summary>
        /// <param name="property"></param>
        /// <param name="stopAt"></param>
        /// <returns></returns>
        private static string RecomposeProperty(string property, int stopAt)
        {
            var propertyParts = new List<string>(property.Split('.'));
            string recomposedProperty = "o.";

            for (int i = 0; i <= stopAt; i++)
            {
                recomposedProperty += propertyParts[i] + ".";
            }

            return recomposedProperty.Remove(recomposedProperty.Length - 1);
        }

        /// <summary>
        /// Build a join clause from a list of properties.
        /// </summary>
        /// <param name="searchProperties"></param>
        /// <param name="displayProperties"></param>
        /// <returns></returns>
        protected static string GetJoins(string searchProperties, string displayProperties)
        {
            var properties = new List<string>(searchProperties.Split(';'));
            properties.AddRange(displayProperties.Split(';'));

            properties = new List<string>(properties.RemoveDuplicates());

            string joinClause = "";

            var joins = new List<string>();

            foreach (string property in properties)
            {
                AddJoins(property, joins);
            }

            for (int i = 0; i < joins.Count; i++)
            {
                joinClause += string.Format("left join {0} ", joins[i]);
            }

            return joinClause;
        }

        private static void AddJoins(string property, List<string> joins)
        {
            var propertyParts = new List<string>(property.Split('.'));

            if (propertyParts.Count < 2)
                return;

            for (int i = 0; i < propertyParts.Count - 1; i++)
            {
                string recomposedProperty = RecomposeProperty(property, i);
                if (!joins.Contains(recomposedProperty))
                    joins.Add(recomposedProperty);
            }

            return;
        }
    }
}
